본문으로 건너뛰기

23.05.20

오늘 한 일

  • 알고리즘 문제 풀이
  • 포트폴리오 마무리 및 자소서 작성
  • 프로그라피 팀 회의
  • 익스텐션 개발
    • Sentry 적용
      • ErrorBoundary 적용
  • Nest.js 강의 듣기

이력서 작성

이력서는 피그마에 작성하고, 포트폴리오는 노션에 작성했다.

이력서에 조금 더 나에 대한 설명을 추가하고, 포트폴리오에는 프로젝트에 대한 설명을 추가해서 내일까지 마무리해야겠다.

익스텐션 개발

Toast 컴포넌트 구현

Sentry에서 제공하는 ErrorBoundary를 이용하여 에러가 발생하면 토스트를 띄우는 컴포넌트를 구현했다.

// src/components/uis/Toast/index.tsx
type Props = {
message: string;
type: 'success' | 'error';
delay?: number;
};

const Toast = ({ message, type, delay = 3000 }: Props) => {
const [isOpen, setIsOpen] = useState(false);

const toastColor = {
success: 'bg-green-500',
error: 'bg-red-500',
};

useEffect(() => {
setIsOpen(true);
const timer = setTimeout(() => setIsOpen(false), delay);
return () => clearTimeout(timer);
}, []);

return (
<Modal.Background isOpen={isOpen} className='fixed left-0 top-0 z-[1999]'>
<Modal
className={`round-[7px] border-1 fixed left-1/2 top-[20px] flex h-[50px] max-w-[200px] translate-x-[-50%] items-center justify-center rounded-[15px] p-[15px] text-white ${toastColor[type]}`}
>
<p>{message}</p>
</Modal>
</Modal.Background>
);
};
// src/pages/content/App.tsx
<Portal elementId='modal'>
<ErrorBoundary fallback={<Toast message='에러가 발생했습니다.' type='error' />}>
<ContentModal ref={modalRef} onClick={handleModalClick} isOpen={isModalOpen} />
</ErrorBoundary>
</Portal>

ErrorBoundary에서 비동기 에러를 잡지 못하는 문제

왜 못잡을까?

비동기 함수에서 발생한 에러는 일반적인 라이프 사이클 외부에서 발생한 에러고 렌더링 중 발생하지 않기 때문에 ErrorBoundary에서 잡을 수 없다.

공식문서 Error boundaries do not catch errors for:

  • Event handlers (learn more)
  • Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
  • Server side rendering
  • Errors thrown in the error boundary itself (rather than its children)

그래서 try/catch나 catch를 이용하여 에러를 잡아야 한다.

나는 try/catch 문을 사용하지 않고 catch를 이용했다. async/await를 사용하면 내부 코드들이 동기식으로 읽을 수 있는 장점이 있는데, try/catch를 사용하게 되면 코드가 동기식으로 읽히지 않아 async/await의 장점을 깨뜨리는 것 같았다. 그래서 catch를 이용하여 에러를 잡았다.

useError 훅을 만들어서 catch에서 setError를 호출하고, error state가 존재하면 에러를 던져주어 ErrorBoundary에서 잡을 수 있도록 했다.

// src/hooks/useError.ts
const useError = () => {
const [error, setError] = useState<Error | null>(null);

const catchAsyncError = (error: Error) => {
setError(error);
};

return { error, catchAsyncError };
};

예외 처리할 때 throw하지 않고 return을 해주는 경우에도 로그를 남기고 싶다면?

이런 경우에는 Sentry의 captureException를 사용하면 된다.

크롤링 중에 받아와야할 것을 받아오지 못했을 때, 에러를 던지지 않고 return을 해주었는데, 이런 경우에도 로그를 남기고 싶었다. 그래서 catchAsyncError에서 captureException를 호출해주었다.

const getVideoAtCourseDocument = ($: cheerio.CheerioAPI, courseId: string) => {
return $('.total_sections .activity.vod .activityinstance')
.map((i, el) => {
const link = $(el).find('a').attr('href');
if (!link) {
captureException(new Error(`동영상 링크가 없습니다. courseId: ${courseId}`)); // here
return;
}

const id = getLinkId(link);
const title = $(el).find('.instancename').clone().children().remove().end().text().trim();
const [, endAt, timeInfo] = $(el)
.find('.displayoptions')
.text()
.split(/ ~ |,/)
.map((str) => str.trim());

const v: Video = {
type: 'video',
hasSubmitted: false,
id,
courseId,
title,
endAt,
timeInfo,
};

return v;
})
.get();
};

리팩토링

ContentModal 컴포넌트에서 너무 많은 역할을 하고 있어서 분리하는 작업을 했다.

useScrollLock hook

일단 Modal이 켜질 때 뒤에 있는 컨텐츠들이 스크롤되지 않도록 useEffect를 통해 스타일을 바꿔주어 구현했었는데, 이 부분을 훅으로 빼주었다.

const useScrollLock = () => {
const scrollLock = () => {
document.body.style.cssText = `
position: fixed;
top: -${window.scrollY}px;
overflow-y: scroll;
width: 100%;`;
};

const scrollUnlock = () => {
const scrollY = document.body.style.top;
document.body.style.cssText = '';
window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
};

return { scrollLock, scrollUnlock };
};

export default useScrollLock;

useFetchData hook

컴포넌트에서 데이터를 받아오는 로직을 분리하여 훅으로 만들었다.

// src/hooks/useFetchData.ts

type dataType = {
courseList: Course[];
activityList: ActivityType[];
updateAt: number;
};

const useFetchData = () => {
const [pos, setPos] = useState(0);
const [data, setData] = useState<dataType>({
courseList: [{ id: '-1', title: '전체' }],
activityList: [],
updateAt: 0,
});

const getData = async () => {
const courses = await getCourses();
const activities = await allProgress(
courses.map((course) => getActivities(course.id)),
(progress) => setPos(progress)
).then((activities) => activities.flat());

const updateAt = new Date().getTime();

setData({
courseList: [{ id: '-1', title: '전체' }, ...courses],
activityList: activities,
updateAt,
});

setPos(0);

chrome.storage.local.set({
courses,
activities,
updateAt,
});
};

const getLocalData = () => {
chrome.storage.local.get(({ updateAt, courses, activities }) => {
setData({
courseList: [{ id: '-1', title: '전체' }, ...courses],
activityList: activities,
updateAt,
});
});
};

return { getData, getLocalData, data, pos };
};

export default useFetchData;

모달을 열었을 때 업데이트 된 시간이 10분이 지났으면 getData, 아니면 getLocalData를 호출하도록 했다.

// src/components/ContentModal.tsx

const ContentModal = ({ isOpen, onClick }: Props, ref: React.Ref<HTMLDivElement>) => {
const [selectedCourse, setSelectedCourse] = useState<Course>({ id: '-1', title: '전체' });
const [statusType, setStatusType] = useState<{ id: number; title: string }>(status[0]);
const [isRefresh, setIsRefresh] = useState(false);

const { catchAsyncError } = useError();
const { scrollLock, scrollUnlock } = useScrollLock();
const [getData, getLocalData, data, pos] = useFetchData();

const { courseList, activityList, updateAt } = data;

useEffect(() => {
if (!isRefresh) return;
getData()
.then(() => setIsRefresh(false))
.catch(error => catchAsyncError(error));
}, [isRefresh]);

useEffect(() => {
if (!isOpen) return;
scrollLock();
if (!isRefresh)
chrome.storage.local.get(['updateAt'], ({ updateAt }) => {
if (!updateAt) return setIsRefresh(true);

const diff = new Date().getTime() - updateAt;
const isOverRefreshTime = diff > REFRESH_TIME;

if (!isOverRefreshTime) {
getLocalData();
} else {
setIsRefresh(true);
}
});

return scrollUnlock;
}, [isOpen]);

return (
//...
)
}

아직 useEffect를 사용하는 부분이 많아서 리팩토링이 필요하다.

Refresh할 때 로직을 분리해야할 것 같다.


내일 할 일

  • 알고리즘 문제 풀이
  • 포트폴리오 다듬고 자기소개서 작성 후 제출
  • IT 특강 팀플 회의
  • 익스텐션 에타에 홍보?